using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.ComponentModel;
using System.Drawing;
using System.Web.UI.Design;
using System.Collections;
using System.Collections.Specialized;
using System.Configuration;
using System.Text;


using BizElements.BusinessLayer;

namespace BizElements.Web
{
    #region Documentation.
    /// <summary>
    /// Popup/lookup control bound to a <see cref="BizElements.BusinessLayer.IBindableField"/> business class property
    /// which includes at least one displayable field (see <see cref="BizElements.BusinessLayer.IBindableField.SatelliteFields"/>).
	/// </summary>
    /// <remarks><para><b>BoundLookup</b> control is used to select a value from a popup window specified in
    /// the <see cref="PopupUrl"/> property. Popup windows invokes client-side script generated by the lookup 
    /// control to set selected key and display values into lookup control.</para>
    /// <para>Tipically the lookup control is bound, similarly to list controls such as DropDownList,
    /// to a single key property (set and retrieved by <see cref="SelectedValue"/> property) which is hidden from 
    /// the user. The <see cref="BizElements.Web.BoundLookup.Text"/> property of the <b>BoundLookup</b> shows a display member
    /// (see <see cref="BizElements.BusinessLayer.IBindableField.SatelliteFields"/>). Eg. when selecting a country 
    /// from a popup window, the <see cref="SelectedValue"/> will hold <b>CountryId</b> value and the 
    /// <see cref="BizElements.Web.BoundLookup.Text"/> will hold <b>CountryName</b>.</para>
    /// <para><b>BoundLookup</b> control is not limited to only one display member. Other satellite fields
    /// specified in the <see cref="BizElements.BusinessLayer.IBindableField.SatelliteFields"/> property of a
    /// <see cref="BizElements.BusinessLayer.IBindableField"/> instance can be displayed in other HTML <b>input</b> 
    /// controls on the form whose IDs are specified in the <see cref="AdditionalDisplayControls"/>.</para></remarks>
    #endregion
    [DefaultEvent("ValueSelected")]
    [ToolboxData("<{0}:BoundLookup runat=server></{0}:BoundLookup>")]
	[DesignerAttribute(typeof(BoundLookupDesigner), typeof(System.ComponentModel.Design.IDesigner))]
    public class BoundLookup : TextBox, IBoundControl, IValidator, ISearchPageClient, IBoundLookupControl
    {
        Label errorLbl = new Label();

        #region Enums.

        /// <summary>
        /// Determines the position of popup/iframe element with regard to referent element (textbox).
        /// </summary>
        public enum PopupPositionMode
        {
            /// <summary>
            /// Indicates that popup is absolutely positioned on the page as determined by <see cref="PopupTopPx"/> and <see cref="PopupLeftPx"/> properties.
            /// </summary>
            Absolute,

            /// <summary>
            /// Indicates that popup is positioned below the textbox.
            /// </summary>
            Below
        }

        #endregion

        #region FieldPermissions.

        FieldAccessMode accessMode = FieldAccessMode.Automatic;
		string editCss = "";
		string readOnlyCss = "";

		IBindableField bizField;
        Color errorColor = Color.Red;
		ErrorLocation errorLocation = ErrorLocation.Below;
        int displayFieldsCount = 1;
        string displayFieldsDelimiter = " ";

        int popupLeftPx;
        int popupTopPx;
        string selectedValueQueryKey;
        string textQueryKey;
		string additionalDisplayControls = "";
        bool autoPopupOnChange;
        bool autoPostBackOnClearOrSelect;
        PopupPositionMode popupPosition = PopupPositionMode.Absolute;
        bool updateUrlOnKeyPress;
        int updateUrlMinKeyDelayInMs = 1000;
        int updateUrlMinChars = 3;
        string onClientValueSelected;

		TextBox selectedValueTxt = new TextBox();
		HtmlImage lookupBtn = new HtmlImage();
		HtmlImage deleteBtn = new HtmlImage();
		HtmlGenericControl div = new HtmlGenericControl("div");
		HtmlGenericControl iframe = new HtmlGenericControl("iframe");
        Button btnAutoPostBack = new Button();
        TextBox txtPopupUrl = new TextBox();
		
		// If set to value other then null then script names will not be automatically created,
		// instead the provided names will be used.
		string forcedHideLookupJScript;
		string forcedDisplayValuesFunctionArgumentCount;

		#endregion

		#region Contructors.

		/// <summary>
		/// Initializes a new instance of the <see cref="BoundLookup"/> class.
		/// </summary>
		public BoundLookup()
		{
            // Control will automatically load PostBack and ViewState (when Visible=false) values.
            this.Controls.Add(this.selectedValueTxt);

			this.selectedValueTxt.Style["display"] = "none";
            this.btnAutoPostBack.Style["display"] = "none";
            this.txtPopupUrl.Style["display"] = "none";

			this.lookupBtn.Align = "top";
            this.lookupBtn.Style["cursor"] = "pointer";
			
			this.deleteBtn.Align = "top";
            this.deleteBtn.Style["cursor"] = "pointer";
			
			this.div.Style["position"] = "absolute";
			this.div.Style["z-index"] = "-1";
			this.div.Style["visibility"] = "hidden";
            this.div.Attributes["onclick"] = "javascript:return false;";
            this.popupLeftPx = 50;
            this.popupTopPx = 50;
			
			this.iframe.Attributes["frameBorder"] = "0";
			this.iframe.Attributes["width"] = "600px";
			this.iframe.Attributes["height"] = "500px";
            this.iframe.Attributes["allowTransparency"] = "true"; // IE requirement.
			this.div.Controls.Add(this.iframe);
		}

		#endregion

        #region OnInit.

        /// <summary>
        /// Raises the <b>Init</b> event.
        /// </summary>
        /// <param name="e">An <b>EventArgs</b> object that contains the event data.</param>
        protected override void OnInit(EventArgs e)
        {
            base.OnInit(e);
            AssignIDsToChildControls();
            AddMyselfToParentPageValidators();
            Page.ClientScript.RegisterClientScriptResource(this.GetType(), "BizElements.Web.BoundLookup.js");
        }

        private void AddMyselfToParentPageValidators()
        {
            this.Page.Validators.Add(this);
        }

        #endregion

        #region AssignIDsToChildControls.

        private void AssignIDsToChildControls()
		{
            this.div.ID = this.ClientID + "__Div";
            this.iframe.ID = this.ClientID + "__Frame";
            this.btnAutoPostBack.ID = this.ClientID + "__AutoPostBack";
            this.txtPopupUrl.ID = this.ClientID + "__Url";
        }

        #endregion

        #region OnLoad.

        /// <summary>
        /// Raises the <b>Init</b> event.
        /// </summary>
        /// <param name="e">An <b>EventArgs</b> object that contains the event data.</param>
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);            
            if (this.Page.IsPostBack)
            {
                // Sometimes, Text property is not set to the value set in the client-side script.
                // Don't know why that is happening. Manually inspect POST variables.
                // POST variables are null if Visible=false. In that case controls manage to 
                // automatically load values from ViewState.
                if (this.ClientID != null  &&  this.Page.Request[this.ClientID] != null)
                    this.Text = this.Page.Request[this.ClientID];

                if (this.selectedValueTxt.ClientID != null && this.Page.Request[this.selectedValueTxt.ClientID] != null)
                    this.selectedValueTxt.Text = this.Page.Request[this.selectedValueTxt.ClientID];

                // Manually inspect POST variables to determine wheter ValueSelected event occured.
                FireValueSelectedEventIfItOccured();
            }
        }

        #endregion

        #region Error properties.

        /// <summary>
        /// Gets or sets the CSS Class name applied to the error label.
        /// </summary>
        [Bindable(false), Category("Appearance"), DefaultValue(null),
        Description("CSS Class name applied to the error label.")]
        public virtual string ErrorCss
        {
            get { return this.errorLbl.CssClass; }
            set { this.errorLbl.CssClass = value; }
        }

        /// <summary>
        /// Gets or sets the color of the error text.
        /// </summary>
        [Bindable(false), Category("Appearance"), DefaultValue(typeof(Color), "Red"),
        Description("Color of the error text.")]
        [TypeConverter(typeof(WebColorConverter))]
        public virtual Color ErrorColor
        {
            get { return this.errorColor; }
            set { this.errorColor = value; }
        }

        /// <summary>
        /// Gets or sets the error text displayed in the error label or in the page's validation summary. 
        /// Usually set programmatically.
        /// </summary>
        [Bindable(false), Category("Appearance"),
        Description("Error text displayed in the error label or in the page's validation summary. Usually set programmatically.")]
        [DefaultValue("")]
        public virtual string ErrorText
        {
            get { return this.errorLbl.Text; }
            set { this.errorLbl.Text = value; }
        }

        /// <summary>
        /// Gets or sets the indicator placed by the control if it contains an invalid value. 
        /// Displayed only when the error text is displayed in the validation summary.
        /// </summary>
        [Bindable(false), Category("Appearance"), DefaultValue("*"),
        Description("Indicator placed by the control if it contains an invalid value. Displayed only when the error text is displayed in the validation summary.")]
        public virtual string ErrorIndicator
        {
            get { return "*"; }
            set { }
        }

        /// <summary>
        /// Gets or sets the value which specifies where the error text is located.
        /// </summary>
        [Bindable(false), Category("Appearance"), DefaultValue(typeof(ErrorLocation), "Below"),
        Description("Specifies where the error text is located.")]
        public virtual ErrorLocation ErrorLocation
        {
            get { return this.errorLocation; }
            set { this.errorLocation = value; }
        }

		#endregion

		#region IBoundControl.

		/// <summary>
		/// Gets or sets the value which indicates whether the <b>BoundLookup</b> can change data in the bound field.
		/// </summary>
		/// <remarks>Setting this property to <b>ReadOnly</b> will hide lookup and clear action buttons.</remarks>
		[Bindable(false), Category("Behavior"), DefaultValue(typeof(FieldAccessMode), "Automatic"),
		Description("Indicates whether the contents of the control can be changed.")]
		public FieldAccessMode AccessMode
		{
			get {return this.accessMode;}
			set {this.accessMode = value;}
		}

		/// <summary>
		/// Gets or sets the business class' property to which the control is bound. Set programmatically.
		/// </summary>
		[Description("Business class' property to which the control is bound. Set programmatically.")]
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public virtual IBindableField BusinessField
		{
			get {return this.bizField;}
			set {this.bizField = value;}
		}

		/// <summary>
		/// Appends all errors in the collection to the <see cref="ErrorText"/>.
		/// </summary>
		/// <param name="rules">Collection of broken rules.</param>
		/// <param name="formatter">The <see cref="IMessageFormatter"/> object used to format the messages contained in the collection.</param>
		public virtual void AppendError(BusinessRuleCollection rules, IMessageFormatter formatter)
		{
			BusinessRuleCollection brokenRulesForThisControl = rules.GetRulesForField(this.BusinessField.BindingName, true);
			this.ErrorText += brokenRulesForThisControl.ToString(formatter) + " ";
		}

		/// <summary>
		/// Clears errors. Same as setting the <see cref="ErrorText"/> to empty string.
		/// </summary>
		public virtual void ClearError()
		{
			this.ErrorText = "";
		}

		#endregion

		#region Control properties.

        /// <summary>
        /// Gets or sets the text displayed in the <b>BoundLookup</b> control.
        /// </summary>
        public override string Text
        {
            get { return base.Text; }
            set { base.Text = value; }
        }

		/// <summary>
		/// Gets or sets the Cascading Style Sheet (CSS) class rendered by the Web server control on the client.
		/// </summary>
		[Bindable(false), Category("Behavior"), DefaultValue(typeof(string), ""),
		Description("Cascading Style Sheet (CSS) class rendered by the Web server control on the client.")]
		public override string CssClass
		{
			get { return this.editCss; }
			set
			{
				this.editCss = (value != null) ? value.Trim() : "";
				base.CssClass = this.editCss;
			}
		}

		/// <summary>
		/// Gets or sets the Cascading Style Sheet (CSS) class rendered by the Web server control on the client 
        /// when the control is in read-only mode.
		/// </summary>
		[Bindable(false), Category("Behavior"), DefaultValue(typeof(string), ""),
		Description("Cascading Style Sheet (CSS) class rendered by the Web server control on the client when the control is in read-only mode.")]
		public string ReadOnlyCssClass
		{
			get {return this.readOnlyCss;}
			set {this.readOnlyCss = (value != null) ? value.Trim() : "";}
		}

		/// <summary>
		/// Gets or sets the value selected in the popup/lookup.
		/// </summary>
        [DefaultValue("")]
        [Description("Value selected in the lookup.")]
		public virtual string SelectedValue
		{
			get { return this.selectedValueTxt.Text; }
			set { this.selectedValueTxt.Text = value; }
		}

		/// <summary>
		/// Gets or sets the programmatic identifier assigned to the server control.
		/// </summary>
		[Bindable(false), Category("Misc"),
		Description("Programmatic identifier assigned to the server control.")]
		public override string ID
		{
			get { return base.ID; }
			set
			{
				base.ID = value;
                AssignIDsToChildControls();				
			}
		}

        /// <summary>
        /// Gets the TextBox control which contains the <b>SelectedValue</b>.
        /// </summary>
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public virtual TextBox SelectedValueTextBox
        {
            get { return this.selectedValueTxt; }
        }

		/// <summary>
		/// Gets or sets the URL of the popup page which performs the lookup action.
		/// </summary>
		[Bindable(false), Category("Behavior"),
		Description("URL of the popup page which performs the lookup action.")]
        [DefaultValue("")]
		public virtual string PopupUrl
		{
			get {return this.txtPopupUrl.Text;}
            set { this.txtPopupUrl.Text = (value != null) ? value.Trim() : ""; }
		}

        /// <summary>
        /// Gets or sets the optional popup URL query key by which the current <see cref="SelectedValue"/> 
        /// property value is passed to popup page.
        /// </summary>
        [Bindable(false), Category("Behavior"), DefaultValue(null),
        Description("Optional popup URL query key by which the current SelectedValue property value is passed to popup page.")]
        public virtual string SelectedValueQueryKey
        {
            get { return this.selectedValueQueryKey; }
            set { this.selectedValueQueryKey = value; }
        }

        /// <summary>
        /// Gets or sets the optional popup URL query key by which the current <see cref="Text"/> 
        /// property value is passed to popup page.
        /// </summary>
        [Bindable(false), Category("Behavior"), DefaultValue(null),
        Description("Optional popup URL query key by which the current Text property value is passed to popup page.")]
        public virtual string TextQueryKey
        {
            get { return this.textQueryKey; }
            set { this.textQueryKey = value; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the popup page is automatically openned after the text is modified.
        /// </summary>
        [Bindable(false), Category("Behavior"), DefaultValue(typeof(bool), "false"),
        Description("Automatically open the popup page after the text is modified.")]
        public virtual bool AutoPopupOnChange
        {
            get { return this.autoPopupOnChange; }
            set { this.autoPopupOnChange = value; }
        }

		/// <summary>
		/// Gets or sets the height of the popup window/iframe.
		/// </summary>
		[Bindable(false), Category("Appearance"),
		Description("Height of the popup window/iframe.")]
        [DefaultValue(typeof(Unit), "500px")]
		public virtual Unit PopupHeight
		{
			get { return Unit.Parse(this.iframe.Attributes["height"]); }
			set { this.iframe.Attributes["height"] = value.ToString(); }
		}

		/// <summary>
        /// Gets or sets the popup position in pixels relative to the left edge of the document.
		/// </summary>
		[Bindable(false), Category("Appearance"),
        Description("Popup position in pixels relative to the left edge of the document.")]
        [DefaultValue(50)]
		public virtual int PopupLeftPx
		{
            get { return this.popupLeftPx; }
			set 
            {
                this.popupLeftPx = value;
                this.div.Style["left"] = value.ToString() + "px";
            }
		}

		/// <summary>
        /// Gets or sets the popup position in pixels relative to the top edge of the document.
		/// </summary>
		[Bindable(false), Category("Appearance"),
        Description("Popup position in pixels relative to the top edge of the document.")]
        [DefaultValue(50)]
		public virtual int PopupTopPx
		{
            get { return this.popupTopPx; }
			set 
            {
                this.popupTopPx = value;
                this.div.Style["top"] = value.ToString() + "px";
            }
		}

		/// <summary>
		/// Gets or sets the width of the popup window/iframe.
		/// </summary>
		[Bindable(false), Category("Appearance"),
		Description("Width of the popup window/iframe.")]
        [DefaultValue(typeof(Unit), "600px")]
		public virtual Unit PopupWidth
		{
            get { return Unit.Parse(this.iframe.Attributes["width"]); }
            set { this.iframe.Attributes["width"] = value.ToString(); }
		}

        /// <summary>
        /// Gets or sets the list of semicolon delimited client IDs of the HTML <b>input</b> controls which display additional fields.
        /// </summary>
        /// <remarks>BoundLookup control may populate other controls with the values returned by lookup page.</remarks>
        [Bindable(false), Category("Appearance"),
        Description("Semicolon delimited IDs of the text controls which display additional fields.")]
        [DefaultValue("")]
        public string AdditionalDisplayControls
        {
            get { return this.additionalDisplayControls; }
            set { this.additionalDisplayControls = value; }
        }

        /// <summary>
        /// Gets or sets the URL of the lookup button image.
        /// </summary>
        [Bindable(false), Category("Appearance"),
        Description("URL of the lookup button image.")]
        [DefaultValue("")]
        public string LookupImageUrl
        {
            get { return this.lookupBtn.Src; }
            set { this.lookupBtn.Src = value; }
        }

        /// <summary>
        /// Gets or sets the URL of the clear button image.
        /// </summary>
        [Bindable(false), Category("Appearance"),
        Description("URL of the clear button image.")]
        [DefaultValue("")]
        public string ClearImageUrl
        {
            get { return this.deleteBtn.Src; }
            set { this.deleteBtn.Src = value; }
        }

        /// <summary>
        /// Gets or sets a value that indicates whether an automatic post back to the server occurs when the user clears or selects lookup values.
        /// </summary>
        [DefaultValue(false)]
        [Category("Behavior")]
        [Description("Indicates whether an automatic post back to the server occurs whenever the user clears or selects lookup values.")]
        public bool AutoPostBackOnClearOrSelect
        {
            get { return this.autoPostBackOnClearOrSelect; }
            set { this.autoPostBackOnClearOrSelect = value; }
        }                

        /// <summary>
        /// Gets or sets how the popop window/iframe is positioned on the page.
        /// </summary>
        [Category("Layout")]
        [DefaultValue(typeof(PopupPositionMode), "Absolute")]
        [Description("How the popop window/iframe is positioned on the page.")]
        public PopupPositionMode PopupPosition
        {
            get { return this.popupPosition; }
            set { this.popupPosition = value; }
        }

        /// <summary>
        /// Gets or sets whether the URL of the popup is automatically updated when the user changes the <see cref="Text"/>.
        /// </summary>
        /// <value><b>true</b> if the modified contents of <see cref="Text"/> property are automatically appended
        /// to the <see cref="PopupUrl"/> through <see cref="TextQueryKey"/> query key; otherwise <b>false</b>.
        /// Default is <b>false</b>.</value>
        /// <remarks><para>This my be used to achieve the effect of autopopulated combobox.</para>
        /// <para>Has no effect if the <see cref="TextQueryKey"/> property is not set.</para></remarks>
        [DefaultValue(false)]
        [Category("Behavior")]
        [Description("Whether the URL of the popup is automatically updated when the user changes the Text.")]
        public virtual bool UpdateUrlOnKeyPress
        {
            get { return this.updateUrlOnKeyPress; }
            set { this.updateUrlOnKeyPress = value; }
        }

        /// <summary>
        /// Gets or sets the number of milliseconds <b>BoundLookup</b> waits after the last key press occured 
        /// before it updates the URL of the popup window.
        /// </summary>
        /// <value>Number of milliseconds <b>BoundLookup</b> waits after the last key press occure before 
        /// it updates the URL of the popup window. No delay occures if the value is zero or a negative number.
        /// Default is <b>1000</b> milliseconds.</value>
        /// <remarks>Has no effect if the <see cref="UpdateUrlOnKeyPress"/> is set to <b>false</b> or if 
        /// the <see cref="TextQueryKey"/> property is not set.</remarks>
        [DefaultValue(1000)]
        [Category("Behavior")]
        [Description("Number of milliseconds BoundLookup waits after the last key press occured before it updates the URL of the popup window.")]
        public virtual int UpdateUrlMinKeyPressDelayInMs
        {
            get { return this.updateUrlMinKeyDelayInMs; }
            set { this.updateUrlMinKeyDelayInMs = value; }
        }

        /// <summary>
        /// Gets or set the minimum number of chars a user has to enter before the URL of the popup window/iframe
        /// is automatically updated and the popup is displayed.
        /// </summary>
        /// <value>Minimum number of chars a user has to enter before the URL of the popup window/iframe is 
        /// automatically updated and the popup is displayed. Default is <b>3</b>.</value>
        /// <remarks>Has no effect if the <see cref="UpdateUrlOnKeyPress"/> is set to <b>false</b> or if 
        /// the <see cref="TextQueryKey"/> property is not set.</remarks>
        [DefaultValue(3)]
        [Category("Behavior")]
        [Description("Minimum number of chars a user has to enter before the URL of the popup window/iframe is automatically updated and the popup is displayed.")]
        public virtual int UpdateUrlMinChars
        {
            get { return this.updateUrlMinChars; }
            set { this.updateUrlMinChars = value; }
        }

        bool fieldCaptionIsDefaultToolTip = true;

        /// <summary>
        /// Gets or sets whether the caption of the bound field is used as default tooltip.
        /// </summary>
        /// <value><b>true</b> if field caption is default tooltop; <b>false</b> otherwise. Default is <b>true</b>.</value>
        [Bindable(false)]
        [Category("Behavior")]
        [DefaultValue(true)]
        [Description("Whether the caption of the bound field is used as default tooltip.")]
        public virtual bool FieldCaptionIsDefaultToolTip
        {
            get { return this.fieldCaptionIsDefaultToolTip; }
            set { this.fieldCaptionIsDefaultToolTip = value; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether the Web server control is enabled.
        /// </summary>
        public override bool Enabled
        {
            get { return base.Enabled; }
            set
            {
                base.Enabled = value;
                this.lookupBtn.Disabled = !value;
                this.deleteBtn.Disabled = !value;
            }
        }

        /// <summary>
        /// Gets or sets the number of satellite/display fields which are displayed in the <see cref="Text"/> property of the <b>BoundLookup</b> control.
        /// </summary>
        /// <value>The number of display fields displayed in the Text property. Default is <b>1</b>. Zero and negative values are converted to <b>1</b>.</value>
        [Category("Behavior")]
        [DefaultValue(1)]
        [Description("The number of display fields displayed in the Text property.")]
        public virtual int DisplayFieldsCount
        {
            get { return this.displayFieldsCount; }
            set { this.displayFieldsCount = (value < 1) ? 1 : value; }
        }

        /// <summary>
        /// Gets or sets the string which delimites the values of satellite/display fields displayed in the <see cref="Text"/> property.
        /// </summary>
        /// <value>A string. Default is one space <b>" "</b>. Nulls are converted to empty strings.</value>
        /// <seealso cref="DisplayFieldsCount"/>
        [Category("Behavior")]
        [DefaultValue(" ")]
        [Description("The string which delimites the values of satellite/display fields displayed in the Text property.")]
        public virtual string DisplayFieldsDelimiter
        {
            get { return this.displayFieldsDelimiter; }
            set { this.displayFieldsDelimiter = value ?? ""; }
        }

        /// <summary>
        /// Gets or sets the client-side script that executes when a user clears or selects lookup values.
        /// </summary>
        [Category("Behavior")]
        [DefaultValue(null)]
        [Description("Client-side script that executes when a user clears or selects lookup values.")]
        [ThemeableAttribute(false)]
        public string OnClientValueSelected
        {
            get { return this.onClientValueSelected; }
            set { this.onClientValueSelected = value; }
        }

		#endregion		

        #region Client-side function names.

        /// <summary>
        /// Gets or sets the name of the client-side script which hides the popup window.
        /// </summary>
        /// <remarks>Hide function requires no parameters.</remarks>
        [Browsable(false)]
        [DefaultValue("")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public string HideLookupJScript
		{
			get 
			{
				if (this.forcedHideLookupJScript == null)
					return this.ClientID + "__Hide";
				else
					return this.forcedHideLookupJScript;
			}
			set
			{
				if (value != null  &&  value.Trim() != "")
					this.forcedHideLookupJScript = value.Trim();
				else
					this.forcedHideLookupJScript = null;
			}
		}

        /// <summary>
        /// Gets or sets the name of the client-side script which is used to set the values selected by the popup
        /// window into <b>BoundLookup</b> control.
        /// </summary>
        /// <remarks><para>The generated script requires at least two parameters. The first parameter sets the 
        /// hidden value of <see cref="SelectedValue"/> property, and the other sets the values which is displayed 
        /// to the user (see <see cref="BizElements.Web.BoundLookup.Text"/>).</para>
        /// <para>Number of parameters is greater than two if <see cref="AdditionalDisplayControls"/> contains
        /// IDs of other HTML <b>input</b> elements. One parameter per additional display control is generated.</para>
        /// </remarks>
        [Browsable(false)]
        [DefaultValue("")]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
		public string DisplayValuesJScript
		{
			get 
			{
				if (this.forcedDisplayValuesFunctionArgumentCount == null)
					return this.ClientID + "__SetVals";
				else
					return this.forcedDisplayValuesFunctionArgumentCount;
			}
			set
			{
				if (value != null  &&  value.Trim() != "")
					this.forcedDisplayValuesFunctionArgumentCount = value.Trim();
				else
					this.forcedDisplayValuesFunctionArgumentCount = null;
			}
        }

        #endregion
       
        #region IValidator.

        /// <summary>
		/// Gets the value contained in the <see cref="ErrorText"/> property if a validation summary has been specified as location for errors.
		/// </summary>
		string IValidator.ErrorMessage
		{
			get 
			{
				if (this.errorLocation == ErrorLocation.ValidationSummary)
					return this.ErrorText;
				else
					return "";
			}
			set 
			{				
			}
		}		

		/// <summary>
		/// Does nothing. Validation logic is performed elsewhere (eg. business class).
		/// </summary>
		void IValidator.Validate()
		{
		}

		/// <summary>
		/// Gets a value which indicates whether an the <see cref="ErrorText"/> property contains error description.
		/// </summary>
		bool IValidator.IsValid
		{
			get
			{
				if (this.ErrorText == null  ||  this.ErrorText.Trim() == "")
					return true;
				else
					return false;
			}
			set 
			{
			}
		}

		#endregion

		#region Render.

        const string onkeyupHandlerFormat =
            "if ({0} != null) clearTimeout({0}); "
            + "if (document.getElementById('{3}').value.length >= {4}) {0} = setTimeout('{1}();', {2}); "
            + "else {5}(); ";

        const string InitOldTextFormat = "Biz_BoundLookup_InitOldText(this, '{0}')";
        const string AutoCompleteFormat = "Biz_BoundLookup_AutoComplete(this, '{0}', '{1}', '{2}', {3}, '{4}', {5})";        

        /// <summary>
        /// Raises the PreRender event.
        /// </summary>
        /// <param name="e">An EventArgs object that contains the event data.</param>
        protected override void OnPreRender(EventArgs e)
        {
            // Script blocks must be registered before render phase.
            // The scripts are not required in read-only mode.
            if (!IsInReadOnlyMode())
                RegisterClientScripts(Environment.NewLine);
        }

		/// <summary>
		/// Sends server control content to a provided <see cref="HtmlTextWriter"/> object, which writes the content to be rendered on the client.
		/// </summary>
		/// <param name="output">The <b>HtmlTextWriter</b> object that receives the server control content.</param>
		protected override void Render(HtmlTextWriter output)
		{
            if (this.FieldCaptionIsDefaultToolTip && string.IsNullOrEmpty(this.ToolTip) && this.bizField != null)
                this.ToolTip = bizField.Caption;

            base.AutoPostBack = false;
			if (IsInReadOnlyMode())
			{
				// Hide text-box but render it.
				RenderAsReadOnlyLabel(output);
				Style["display"] = "none";
				this.lookupBtn.Visible = false;
				this.deleteBtn.Visible = false;
			}

            if (this.AutoPopupOnChange)
                this.Attributes["onchange"] = GetDisplayLookupFunctionName() + "();";

            if (this.UpdateUrlOnKeyPress)
            {
                int keyPressTimeOut = (this.UpdateUrlMinKeyPressDelayInMs > 100) ? this.UpdateUrlMinKeyPressDelayInMs : 100;
                int minTextLen = (this.UpdateUrlMinChars >= 0) ? this.UpdateUrlMinChars : 0;
                // Store original value on first keydown.
                this.Attributes["onkeydown"] = string.Format(InitOldTextFormat, GetOldTextValueVariable());
                // Realtime text-change check: catch all keys (including special keys like delete) and compare new value with old/previous value.
                this.Attributes["onkeyup"] = string.Format(AutoCompleteFormat, GetOldTextValueVariable(), GetDisplayLookupFunctionName(), this.HideLookupJScript, minTextLen, GetKeyPressTimeOutIdVariable(), keyPressTimeOut);
            }

			base.Render(output);
			this.selectedValueTxt.RenderControl(output);
            this.btnAutoPostBack.RenderControl(output);
			this.div.RenderControl(output);

            this.txtPopupUrl.Text = WebUtil.ResolveUrl(this.txtPopupUrl.Text);
            this.txtPopupUrl.RenderControl(output);
            if (!string.IsNullOrEmpty(this.txtPopupUrl.Text))
                this.lookupBtn.Attributes["onclick"] = GetDisplayLookupFunctionName() + "();";
			this.lookupBtn.RenderControl(output);

			// Add onClick delete command just before the control is rendered.
			// This ensures that the control ID has been set. ID is required by CreateDeleteScript method.
			if (this.deleteBtn.Attributes["onclick"] != null)
				this.deleteBtn.Attributes["onclick"] = RenderClearLookupValuesScriptBody();
			else
				this.deleteBtn.Attributes.Add("onclick", RenderClearLookupValuesScriptBody());
			this.deleteBtn.RenderControl(output);

			RenderErrorMessage(output);
		}

		private bool IsInReadOnlyMode()
		{
			bool isReadOnly;
			switch (this.AccessMode)
			{
				case FieldAccessMode.Automatic:
					if (this.bizField != null  &&  this.bizField.IsReadOnly)
						isReadOnly = true;
					else
						isReadOnly = false;
					break;
				case FieldAccessMode.Edit:
					isReadOnly = false;
					break;
				case FieldAccessMode.ReadOnly:
					isReadOnly = true;
					break;
				default:
					throw new Exception("Unsupported field access mode: " + this.accessMode.ToString() + ".");
			}

			return isReadOnly;
		}

		private void RenderAsReadOnlyLabel(HtmlTextWriter output)
		{
			Label lbl = new Label();
			lbl.CopyBaseAttributes(this);
			lbl.CssClass = (this.readOnlyCss != "") ? this.readOnlyCss : this.editCss;
			lbl.Text = this.Text;
			lbl.BackColor = this.BackColor;
			lbl.RenderControl(output);
		}

        private void RenderErrorMessage(HtmlTextWriter output)
        {
            if (this.errorLbl.Text != null && this.errorLbl.Text.Trim() != "")
            {
                this.errorLbl.ForeColor = this.ErrorColor;
                this.errorLbl.EnableViewState = false;
                switch (this.errorLocation)
                {
                    case ErrorLocation.Below:
                        output.Write("<br />");
                        break;

                    case ErrorLocation.Right:
                        output.Write("&nbsp;");
                        break;

                    case ErrorLocation.ValidationSummary:
                        output.Write("&nbsp;");
                        break;
                }

                this.errorLbl.RenderControl(output);
            }
        }

        private void RegisterClientScripts(string newLine)
        {
            StringBuilder script = new StringBuilder();
            if (this.UpdateUrlOnKeyPress)
            {
                script.Append(newLine);
                script.Append("var " + GetOldTextValueVariable() + " = null; " + newLine);
                script.Append("var " + GetKeyPressTimeOutIdVariable() + " = null; " + newLine);
            }
            
            script.Append(RenderHideLookupScript());
            script.Append(newLine);
            script.Append(RenderSetLookupValuesScript());
            script.Append(newLine);
            script.Append(RenderDisplayLookupScript());
            script.Append(newLine);

            JScript.RegisterClientScriptBlock("BoundLookup_" + this.ClientID, script.ToString(), true);
        }

        private string GetOldTextValueVariable()
        {
            return this.ClientID + "__OldText";
        }

        private string GetKeyPressTimeOutIdVariable()
        {
            return this.ClientID + "__KeyTimeOutId";
        }

        private string GetDisplayLookupFunctionName()
        {
            return this.ClientID + "__DisplayLookup";
        }

        const string displayLookupCallFormat = "Biz_BoundLookup_Display('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}', {7}, {8}); ";

		private string RenderDisplayLookupScript()
		{
            StringBuilder script = new StringBuilder();
            script.Append("function ");
            script.Append(GetDisplayLookupFunctionName());
            script.Append("(){ ");
            string top = (this.PopupPosition == PopupPositionMode.Absolute) ? this.PopupTopPx.ToString() : "20 + Biz_BoundLookup_GetElementTopOffset('" + this.ClientID + "', true)";
            string left = (this.PopupPosition == PopupPositionMode.Absolute) ? this.PopupLeftPx.ToString() : "Biz_BoundLookup_GetElementLeftOffset('" + this.ClientID + "', true)";
            script.Append(string.Format(displayLookupCallFormat, this.selectedValueTxt.ClientID, this.ClientID, 
                this.ClientID + "__Div", this.ClientID + "__Frame", this.ClientID + "__Url", this.SelectedValueQueryKey, 
                this.TextQueryKey, top, left));
            script.Append("} ");

            return script.ToString();
		}

        private static string GetNextConcatenationOperator(string url)
        {
            return url.Contains("?") ? "&" : "?";
        }

        const string hideLookupCallFormat = "Biz_BoundLookup_Hide('{0}', '{1}', '{2}'); ";

        private string RenderHideLookupScript()
        {
            StringBuilder script = new StringBuilder();
            script.Append("function ");
            script.Append(this.HideLookupJScript);
            script.Append("(){ ");
            script.Append(string.Format(hideLookupCallFormat, this.ClientID, this.ClientID + "__Div", this.ClientID + "__Frame"));
            script.Append("} ");

            return script.ToString();
        }    

		private string RenderSetLookupValuesScript()
		{
            // Script is used to delete values in the lookup after they've been retrieved and processed.
		
			string[] additionalControls = this.AdditionalDisplayControls.Split(';');				

			// Script declaration.
			string script = "function " + this.DisplayValuesJScript + "(";
			script += "sKeyVal, ";
			script += "sDisplayVal";
            int totalDisplayFieldsCount = this.DisplayFieldsCount + additionalControls.Length;
            for (int idxAdditionalField = 2; idxAdditionalField <= totalDisplayFieldsCount; idxAdditionalField++)
                script += ", sDisplayVal" + idxAdditionalField.ToString();

			script += ")";
			script += "{";				

			// Body. Display given values in lookup controls.
			// ClientID (only for child controls) uses double underscores if control belongs to DataGrid.
			// Use UniqueId for main textbox.
			script += "document.getElementById('" + this.selectedValueTxt.ClientID + "').value = sKeyVal; ";
            script += "document.getElementById('" + this.ClientID + "').value = Biz_BoundLookup_TrimString(sDisplayVal";
            string delimiterExpression = " + '" + this.DisplayFieldsDelimiter + "'";
            for (int idxDisplayField=2; idxDisplayField<=this.DisplayFieldsCount; idxDisplayField++)
                script += delimiterExpression + " + sDisplayVal" + idxDisplayField.ToString();
            script += "); ";
				
            int sDisplayIdx = this.DisplayFieldsCount + 1;
			foreach (string ctlId in additionalControls)
			{
				if (ctlId.Trim() != "")
				{
					script += "document.getElementById('" + ctlId + "').value = sDisplayVal" + sDisplayIdx.ToString() + "; ";
					sDisplayIdx++;
				}
			}

			// Hide lookup.
			script += this.HideLookupJScript + "(); ";

            string onClientSelect = TextUtil.TrimNullableString(this.OnClientValueSelected);
            if (!string.IsNullOrEmpty(onClientSelect))
            {
                onClientSelect = TextUtil.EnsureSuffix(onClientSelect, ";");
                script += onClientSelect + " ";
            }

            if (this.AutoPostBackOnClearOrSelect)
                script += "document.getElementById('" + this.btnAutoPostBack.ClientID + "').click(); ";
			script += "}";				

			return script;
		}        

		private string RenderClearLookupValuesScriptBody()
		{
            // Calls SetLookupValues script. All parameters are set to empty strings.
            // SetLookupValues has (1 value field + display fields + additional display controls) parameters.

			int additionalDisplayControlsCount = this.AdditionalDisplayControls.Split(';').Length;
			int totalArgumentCount = 1 + this.DisplayFieldsCount + additionalDisplayControlsCount;

			string setValuesCall = this.DisplayValuesJScript + "(";
			for (int i=0; i<totalArgumentCount-1; i++)
				setValuesCall += "'', ";
			setValuesCall += "'');";

			return setValuesCall;
		}
		
		internal string GetDesignTimeHtml()
		{
			Unit oldWidth = this.PopupWidth;
			Unit oldHeight = this.PopupHeight;
			this.PopupWidth = 0;
			this.PopupHeight = 0;

			System.IO.StringWriter strWriter = new System.IO.StringWriter();
			HtmlTextWriter htmWriter = new HtmlTextWriter(strWriter);
			RenderControl(htmWriter);
			string designTimeHtml = strWriter.GetStringBuilder().ToString();

			this.PopupWidth = oldWidth;
			this.PopupHeight = oldHeight;
			
			return designTimeHtml;
		}        

		#endregion                

        #region ISearchPageClient.

        /// <summary>
        /// Creates a command which accepts the selected search/lookup values and sets them into the client component.
        /// </summary>
        /// <param name="calledFromPopupWindow">Indicates whether the provider is rendered in a popup/iframe window.</param>
        /// <param name="text">Command text.</param>
        /// <param name="requiredParameters">Parameters (ie. data fields) required for the accept command.</param>
        /// <returns><b>IWebCommand</b> object or <b>null</b> is the client doesn't support the accept command.</returns>
        public IWebCommand CreateAcceptCommand(bool calledFromPopupWindow, string text, string[] requiredParameters)
        {
            if (requiredParameters == null || requiredParameters.Length == 0)
                throw new ArgumentException("At least one parameter must be provided.", "requiredParameters");

            StringBuilder callFormat = new StringBuilder();
            if (calledFromPopupWindow)
                callFormat.Append("parent.");
            callFormat.Append(this.DisplayValuesJScript);
            callFormat.Append("(");
            for (int idxParameter = 0; idxParameter < requiredParameters.Length; idxParameter++)
            {
                callFormat.Append("'{");
                callFormat.Append(idxParameter);
                callFormat.Append("}'");
                if (idxParameter < requiredParameters.Length - 1)
                    callFormat.Append(", ");
            }
            callFormat.Append(")");

            JsCommand setValues = new JsCommand(null, text, callFormat.ToString(), requiredParameters);
            return setValues;
        }

        /// <summary>
        /// Creates a command which cancels the search/lookup action.
        /// </summary>
        /// <param name="calledFromPopupWindow">Indicates whether the provider is rendered in a popup/iframe window.</param>
        /// <param name="text">Command text.</param>
        /// <returns><b>IWebCommand</b> object.</returns>
        public IWebCommand CreateCancelCommand(bool calledFromPopupWindow, string text)
        {
            StringBuilder functionCall = new StringBuilder();
            if (calledFromPopupWindow)
                functionCall.Append("parent.");
            functionCall.Append(this.HideLookupJScript);
            functionCall.Append("()");

            return new JsCommand(null, text, functionCall.ToString());
        }

        /// <summary>
        /// Has no meaning. The provider key must be specified via <see cref="ISearchPageClient.SearchPageUrl"/> property.
        /// </summary>
        [Browsable(false)]
        string ISearchClient.SearchProviderKey
        {
            get { return null; }
            set { }
        }

        /// <summary>
        /// Gets or sets the URL of a page which uses/renders the registered <see cref="SearchProvider"/>.
        /// </summary>
        /// <remarks>URL must contain a query string value which specifies the provider key.</remarks>
        [Browsable(false)]
        string ISearchPageClient.SearchPageUrl
        {
            get { return this.PopupUrl; }
            set { this.PopupUrl = value; }
        }

        #endregion                      

        #region ValueSelected client and server-side events.

        /// <summary>
        /// Occurs when a user clears or selects lookup values and <see cref="AutoPostBackOnClearOrSelect"/> property is set to <b>true</b>.
        /// </summary>
        [Description("Fires when a user clears or selects lookup values. Requires that AutoPostBackOnClearOrSelect is set to true.")]
        public event EventHandler ValueSelected;

        private void FireValueSelectedEventIfItOccured()
        {
            // Called in OnLoad event if auto-postback button is clicked.
            if (Page.Request[this.btnAutoPostBack.ClientID] != null)
            {
                if (this.ValueSelected != null)
                    ValueSelected(this, new EventArgs());
            }
        }        

        #endregion

        /// <summary>Gets the control which contains the SelectedValue property.</summary>
        [Description("Control which contains the <b>SelectedValue</b> property.")]
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public Control SelectedValueControl
        {
            get { return this.selectedValueTxt; }
        }

        /// <summary>Gets the control which contains the <b>Text</b> property.</summary>
        [Description("Control which contains the Text property.")]
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public Control TextControl
        {
            get { return this; }
        }

        /// <summary>Gets the name of the control property which is bound two-way to a business object field: <b>SelectedValue</b></summary>
        public string TwoWayBoundProperty
        {
            get { return "SelectedValue"; }
        }

        /// <summary>Gets the names of additional control properties which are bound one-way to business object properties: <b>Text</b></summary>
        public string[] OneWayBoundProperties
        {
            get { return new string[] { "Text" }; }
        }

        /// <summary>Gets error label control which displays errors associated with the given field.</summary>
        [Browsable(false)]
        public ITextControl ErrorLabel
        {
            get { return this.errorLbl; }
        }

        /// <summary>Not implemented. Returns <b>null</b>.</summary>
        [Browsable(false)]
        ITextControl IBoundControl.CaptionLabel
        {
            get { return null; }
        }

        /// <summary>Not implemented. Returns <b>null</b>.</summary>
        [Browsable(false)]
        ITextControl IBoundControl.TooltipLabel
        {
            get { return null; }
        }

        /// <summary>Not implemented. Returns <b>null</b>.</summary>
        [Browsable(false)]
        Control IBoundControl.RequiredFieldIndicatorControl
        {
            get { return null; }
        }
    }	

    /// <summary>
    /// Performs design-time rendering of a <see cref="BoundLookup"/> control.
    /// </summary>
	internal class BoundLookupDesigner : ControlDesigner
    {
        #region GetDesignTimeHtml.

        /// <summary>
        /// Retrieves the HTML markup that is used to represent the control at design time.
        /// </summary>
        /// <returns>The HTML markup used to represent the control at design time.</returns>
        public override string GetDesignTimeHtml()
		{
			BoundLookup lookup = (BoundLookup) this.Component;
            bool wasVisible = lookup.Visible;
            lookup.Visible = true;
			string designTimeHtml = lookup.GetDesignTimeHtml();
            lookup.Visible = wasVisible;
			return designTimeHtml;
        }

        #endregion
    }
}